'use strict';

const readline = require('readline');
const fs = require('fs');
const events = require('events');
const CycleBuffer = require('./cyclebuffer.js');
const SerialPort = require('serialport');

const CMD_WRITE_6502_REQ = 0x00
const CMD_WRITE_6502_RSP = 0x01

const CMD_WRITE_EEPROM_REQ = 0x10
const CMD_WRITE_EEPROM_RSP = 0x11
const CMD_READ_EEPROM_REQ = 0x12
const CMD_READ_EEPROM_RSP = 0x13

const CMD_WRITE_SRAM_REQ = 0x20
const CMD_WRITE_SRAM_RSP = 0x21
const CMD_READ_SRAM_REQ = 0x22
const CMD_READ_SRAM_RSP = 0x23

const CMD_READ_INT_VECTOR_REQ = 0x30
const CMD_READ_INT_VECTOR_RSP = 0x31
const CMD_WRITE_INT_VECTOR_REQ = 0x32
const CMD_WRITE_INT_VECTOR_RSP = 0x33

const CMD_RESET_6502_REQ = 0x40
const CMD_RESET_6502_RSP = 0x41
const CMD_STOP_6502_REQ = 0x42
const CMD_STOP_6502_RSP = 0x43
const CMD_PAUSE_6502_REQ = 0x44
const CMD_PAUSE_6502_RSP = 0x45
const CMD_RESUME_6502_REQ = 0x46
const CMD_RESUME_6502_RSP = 0x47

const CMD_READ_BREAKPOINTS_REQ = 0x50
const CMD_READ_BREAKPOINTS_RSP = 0x51
const CMD_WRITE_BREAKPOINTS_REQ = 0x52
const CMD_WRITE_BREAKPOINTS_RSP = 0x53

const CMD_6502_SEND_REQ = 0x80
const CMD_6502_SEND_RSP = 0x81

const CMD_READY_REQ = 0xb0
const CMD_READY_RSP = 0xb1

const CMD_RSP_STATUS_SUCCESS = 0x00
const CMD_RSP_STATUS_FAILURE = 0x01

const CMD_ECHO_REQ = 0xa0
const CMD_ECHO_RSP = 0xa1


const gReadline = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
});

gReadline.addListener('line', (line) => {
    handleInput(line);
});

const gRspEmitter = new events.EventEmitter();
const gCycleBuffer = new CycleBuffer(8192);

/**
 * @type {SerialPort}
 */
let gSerialPort = null;


let cache = Buffer.alloc(10000);
let offset = 0;
function printCache() {
    if (offset > 0) {
        let msg = cache.toString("ascii", 0, offset);
        offset = 0;
        console.log("[6502]" + msg);
    }
}
setInterval(() => {
    printCache();
}, 1000);
gRspEmitter.on('cmd_6502_send_req', (value) => {
    if (value === '\n') {
        printCache();
    } else {
        offset = cache.writeUInt8(value, offset);
    }
})

/**
 * @param {string} question
 */
async function question(question) {
    return new Promise(function (resolve, reject) {
        gReadline.question(question ? question : '', (answer) => {
            resolve(answer);
        });
    });
}

async function openSerialPort() {
    const portInfos = await SerialPort.list();
    for (let i = 0; i < portInfos.length; i++) {
        const portInfo = portInfos[i];
        console.log(`[${i}]${portInfo.comName}`);
    }
    const line = await question('Select Serial Port:');
    const num = Number.parseInt(line);
    const portInfo = portInfos[num];
    const comName = portInfo.comName;
    gSerialPort = new SerialPort(comName, {
        autoOpen: false,
        baudRate: 1200,
        dataBits: 8,
        stopBits: 1,
        parity: 'none',
    });
    gSerialPort.on('open', (error) => {
        if (error) {
            console.log(`Open Serial Port Error: ${error}`);
            process.exit(1);
        } else {
            console.log(`Open ${comName} success`);
        }
    });
    gSerialPort.on('data', (data) => {
        handleSerialData(data);
    });
    gSerialPort.on('error', (error) => {
        console.log(`Serial Port Error: ${error}`);
        process.exit(1);
    });
    gSerialPort.open();
}

/**
 * @param {Buffer} buffer 
 */
async function writeSerialPort(buffer) {
    return new Promise((resolve, reject) => {
        gSerialPort.write(buffer);
        gSerialPort.drain((error) => {
            resolve(error);
        })
    });
}



/**
 * @param {string} event 
 */
async function cmdRsp(event) {
    return new Promise((resolve, reject) => {
        gRspEmitter.once(event, (msg) => {
            resolve(msg);
        });
    });
}

/**
 * @param {string} msg
 * @returns {Promise<Buffer>}
 */
async function cmdEcho(msg) {
    let buf = Buffer.alloc(2 + msg.length);
    let offset = 0;
    offset = buf.writeUInt8(CMD_ECHO_REQ, offset);
    offset = buf.writeUInt8(msg.length, offset);
    offset = buf.write(msg, offset);
    await writeSerialPort(buf);
    return await cmdRsp('cmd_echo_rsp');
}

/**
 * msg长度不能超过64byte
 * @param {number} addr
 * @param {Buffer} msg 
 */
async function cmdEepromWrite(addr, msg) {
    // CMD ADDRL ADDRH LEN DATA
    let buf = Buffer.alloc(4 + msg.length);
    let offset = 0;
    offset = buf.writeUInt8(CMD_WRITE_EEPROM_REQ, offset);
    offset = buf.writeUInt8(addr & 0xff, offset);
    offset = buf.writeUInt8((addr >> 8) & 0xff, offset);
    offset = buf.writeUInt8(msg.length, offset);
    for (let i = 0; i < msg.byteLength; i++) {
        offset = buf.writeUInt8(msg[i], offset);
    }
    await writeSerialPort(buf);
    await cmdRsp('cmd_write_eeprom_rsp');
}

/**
 * CMD_WRITE_EEPROM_REQ ADDRL ADDRH LEN DATA
 * @param {number} addr 
 * @param {string} filename x0
 */
async function writeEepromFile(addr, filename) {
    const pageSize = 64;
    const content = fs.readFileSync(filename);
    const page = Math.floor(content.byteLength / pageSize);
    for (let i = 0; i < page; i++) {
        const offset = i * pageSize;
        await cmdEepromWrite(addr + offset, content.subarray(offset, offset + pageSize))
        console.log("write offset: " + offset.toString(16));
    }
    const remain = content.byteLength - page * pageSize;
    if (remain > 0) {
        const offset = page * pageSize;
        await cmdEepromWrite(addr + offset, content.subarray(offset, content.byteLength));
    }
    console.log("write eeprom finish");
}


/**
 * CMD_READ_EEPROM_REQ ADDRL ADDRH LEN
 * @param {number} addr 
 * @param {number} len 
 * @returns {Promise<Buffer>}
 */
async function cmdReadEeprom(addr, len) {
    let buf = Buffer.alloc(4)
    let offset = 0;
    offset = buf.writeUInt8(CMD_READ_EEPROM_REQ, offset);
    offset = buf.writeUInt8(addr & 0xff, offset);
    offset = buf.writeUInt8((addr >> 8) & 0xff, offset);
    offset = buf.writeUInt8(len, offset);
    await writeSerialPort(buf);
    return await cmdRsp("cmd_read_eeprom_rsp");
}

/**
 * CMD_WRITE_SRAM_REQ ADDRL ADDRH LEN DATA
 * @param {number} addr 
 * @param {Array<number>} values 
 */
async function cmdWriteSram(addr, values) {
    let buf = Buffer.alloc(4 + values.length);
    let offset = 0;
    offset = buf.writeUInt8(CMD_WRITE_SRAM_REQ, offset);
    offset = buf.writeUInt8(addr & 0xff, offset);
    offset = buf.writeUInt8((addr >> 8) & 0xff, offset);
    offset = buf.writeUInt8(values.length, offset);
    for (let i = 0; i < values.length; i++) {
        offset = buf.writeUInt8(values[i], offset);
    }
    await writeSerialPort(buf);
    await cmdRsp("cmd_write_sram_rsp");
}

/**
 * CMD_READ_SRAM_REQ ADDRL ADDRH LEN
 * @param {number} addr 
 * @param {number} len 
 * @returns {Promise<Buffer>}
 */
async function cmdReadSram(addr, len) {
    let buf = Buffer.alloc(4)
    let offset = 0;
    offset = buf.writeUInt8(CMD_READ_SRAM_REQ, offset);
    offset = buf.writeUInt8(addr & 0xff, offset);
    offset = buf.writeUInt8((addr >> 8) & 0xff, offset);
    offset = buf.writeUInt8(len, offset);
    await writeSerialPort(buf);
    return await cmdRsp("cmd_read_sram_rsp");
}

/**
 * CMD_WRITE_6502_REQ DATA
 * @param {string} msg 
 */
async function cmdWrite6502(char) {
    let buf = Buffer.alloc(2);
    let offset = 0;
    offset = buf.writeUInt8(CMD_WRITE_6502_REQ, offset);
    offset = buf.write(char, offset);
    await writeSerialPort(buf);
    await cmdRsp("cmd_write_6502_rsp");
}

/**
 * CMD_RESET_6502_REQ
 */
async function cmdReset6502() {
    let buf = Buffer.alloc(1);
    let offset = 0;
    offset = buf.writeUInt8(CMD_RESET_6502_REQ, offset);
    await writeSerialPort(buf);
}

/**
 * CMD_STOP_6502_REQ
 */
async function cmdStop6502() {
    let buf = Buffer.alloc(1);
    let offset = 0;
    offset = buf.writeUInt8(CMD_STOP_6502_REQ, offset);
    await writeSerialPort(buf);
}

async function cmdPause6502() {
    let buf = Buffer.alloc(1);
    let offset = 0;
    offset = buf.writeUInt8(CMD_PAUSE_6502_REQ, offset);
    await writeSerialPort(buf);
}

async function cmdResume6502() {
    let buf = Buffer.alloc(1);
    let offset = 0;
    offset = buf.writeUInt8(CMD_RESUME_6502_REQ, offset);
    await writeSerialPort(buf);
}

/**
 * CMD_WRITE_BREAKPOINTS_REQ LEN ADDRL0 ADDRH0 ...
 * CMD_WRITE_BREAKPOINTS_RSP STATUS
 * @param {Array<number>} addrs 
 */
async function cmdWriteBreakpoints(addrs) {
    let buf = Buffer.alloc(2 + 2 * addrs.length);
    let offset = 0;
    offset = buf.writeUInt8(CMD_WRITE_BREAKPOINTS_REQ, offset);
    offset = buf.writeUInt8(2 * addrs.length, offset);
    for (let i = 0; i < addrs.length; i++) {
        offset = buf.writeUInt8(addrs[i] & 0xff, offset);
        offset = buf.writeUInt8((addrs[i] >> 8) & 0xff, offset);
    }
    await writeSerialPort(buf);
    await cmdRsp("cmd_write_breakpoints_rsp");
}

/**
 * @param {string} line
 */
async function handleInput(line) {
    line = line.trim();

    if (line.startsWith('quit')) {
        quit();
    } else if (line.startsWith('echo')) {
        const msg = line.slice('echo'.length).trim();
        console.log("echo [" + msg + "]");
        let rspMsg = await cmdEcho(msg);
        console.log("echo rsp [" + rspMsg + "]");
    } else if (line.startsWith("write eeprom")) {
        const args = line.slice('write eeprom'.length).trim();
        const items = args.split(/\s+/);

        if (items.length === 1) {
            // TODO handle *.hex file
            await writeEepromFile(0x8000, items[0]);
        } else if (items.length === 2) {
            let addr = parseInt(items[0]);
            await writeEepromFile(addr, items[1]);
        } else {
            usage();
        }
    } else if (line.startsWith("read eeprom")) {
        const args = line.slice("read eeprom".length).trim();
        const items = args.split(/\s+/);

        if (items.length === 2) {
            let addr = parseInt(items[0]);
            let len = parseInt(items[1]);
            let buf = await cmdReadEeprom(addr, len);
            for (let i = 0; i < buf.length; i++) {
                process.stdout.write(buf[i].toString(16) + ",");
            }
            console.log("");
        } else {
            usage();
        }
    } else if (line.startsWith("write sram")) {
        const args = line.slice("write sram".length).trim();
        const items = args.split(/\s+/);
        const addr = parseInt(items[0]);
        let values = [];
        for (let i = 1; i < items.length; i++) {
            values[i-1] = parseInt(items[i]);
        }
        await cmdWriteSram(addr, values);
    } else if (line.startsWith("read sram")) {
        const args = line.slice("read sram".length).trim();
        const items = args.split(/\s+/);

        if (items.length === 2) {
            let addr = parseInt(items[0]);
            let len = parseInt(items[1]);
            let buf = await cmdReadSram(addr, len);
            for (let i = 0; i < buf.length; i++) {
                process.stdout.write(buf[i].toString(16) + ",");
            }
            console.log("");
        } else {
            usage();
        }
    } else if (line.startsWith("ascii")) {
        const msg = line.slice("ascii".length).trim();
        for (let i = 0; i < msg.length; i++) {
            await cmdWrite6502(msg[i]);
        }
    } else if (line.startsWith("reset")) {
        await cmdReset6502();
    } else if (line.startsWith("stop")) {
        await cmdStop6502();
    } else if (line.startsWith("pause")) {
        await cmdPause6502();
    } else if (line.startsWith("resume")) {
        await cmdResume6502();
    } else if (line.startsWith("write breakpoints")) {
        const args = line.slice("write breakpoints".length).trim();
        const items = args.split(/\s+/);
        let addrs = [];
        for (let i = 0; i < items.length; i++) {
            addrs[i] = parseInt(items[i]);
        }
        await cmdWriteBreakpoints(addrs);
    } else {
        usage();
    }
}


function usage() {
    console.log(
        `
Usage:
  ascii <ascii string>              send ascii string to 6502 (convert to hex string)
  hex <hex string>                  send hex string to 6502

  write eeprom [<addr>] <xxx.bin>   write bin file to eeprom
  read eeprom addr len              read eeprom

  write sram addr value0 ...        write sram
  read sram addr len                read sram

  write breakpoints addr0 ...       write breakpoints
  reset                             reset 6502
  stop                              stop 6502
  pause                             pause 6502
  resume                            resume 6502
  echo <string>                     echo with 6502 bios
  quit                              quit
Todo:
  write eeprom <xxx.hex>            write hex file to eeprom
  read int vector                   read int vector
  write int vector nmi rst irq      write int vector
`
    );
}

function quit() {
    if (gSerialPort) {
        gSerialPort.close();
    }
    if (gReadline) {
        gReadline.close();
    }
    console.log('Quit!!!');
    process.exit(0);
}

/**
 * @param {Buffer} data
 */
function handleSerialData(data) {
    process.stdout.write("Serial Recv: ")
    for (let i = 0; i < data.length; i++) {
        process.stdout.write(data[i].toString(16) + " ");
    }
    console.log("");

    gCycleBuffer.writeBuffer(data);
    let next = 0;
    do {
        next = 0;

        const len = gCycleBuffer.length();
        if (len === 0) break;

        const cmd = gCycleBuffer.get(0);
        switch (cmd) {
            case CMD_READY_REQ:
                gCycleBuffer.skip(1);
                console.log("6502 Ready!!!");

                next = 1;
                break;
            case CMD_ECHO_RSP:
                // CMD_ECHO_RSP LEN DATA
                if (len >= 2) {
                    let dataLen = gCycleBuffer.get(1)
                    if (len >= 2 + dataLen) {
                        let buf = gCycleBuffer.readBuffer(dataLen, 2);
                        gRspEmitter.emit('cmd_echo_rsp', buf);

                        next = 1;
                    }
                }
                break;
            case CMD_WRITE_EEPROM_RSP:
                // CMD_WRITE_EEPROM_RSP STATUS
                if (len >= 2) {
                    gCycleBuffer.skip(1);
                    let status = gCycleBuffer.read();
                    gRspEmitter.emit('cmd_write_eeprom_rsp', status);

                    next = 1;
                }
                break;
            case CMD_6502_SEND_REQ:
                // CMD_6502_SEND_REQ VALUE
                if (len >= 2) {
                    gCycleBuffer.skip(1);
                    let value = gCycleBuffer.read();
                    //console.log("[6502x]" + value.toString(16) + " " + String.fromCharCode(value));
                    gRspEmitter.emit('cmd_6502_send_req', value);

                    next = 1;
                }
                break;
            case CMD_WRITE_6502_RSP:
                // CMD_WRITE_6502_RSP STATUS
                if (len >= 2) {
                    gCycleBuffer.skip(1);
                    let status = gCycleBuffer.read();
                    gRspEmitter.emit('cmd_write_6502_rsp', status);

                    next = 1;
                }
                break;
            case CMD_READ_EEPROM_RSP:
                // CMD_READ_EEPROM_RSP LEN DATA
                if (len >= 2) {
                    let dataLen = gCycleBuffer.get(1);
                    if (len >= 2 + dataLen) {
                        let buf = gCycleBuffer.readBuffer(dataLen, 2);
                        gRspEmitter.emit("cmd_read_eeprom_rsp", buf);

                        next = 1;
                    }
                }
                break;
            case CMD_WRITE_SRAM_RSP:
                // CMD_WRITE_SRAM_RSP STATUS
                if (len >= 2) {
                    gCycleBuffer.skip(1);
                    let status = gCycleBuffer.read();
                    gRspEmitter.emit("cmd_write_sram_rsp", status);

                    next = 1;
                }

                break;
            case CMD_READ_SRAM_RSP:
                // CMD_READ_SRAM_RSP LEN DATA
                if (len >= 2) {
                    let dataLen = gCycleBuffer.get(1);
                    if (len >= 2 + dataLen) {
                        let buf = gCycleBuffer.readBuffer(dataLen, 2);
                        gRspEmitter.emit("cmd_read_sram_rsp", buf);

                        next = 1;
                    }
                }
                break;
            case CMD_WRITE_BREAKPOINTS_RSP:
                // CMD_WRITE_BREAKPOINTS_RSP STATUS
                if (len >= 2) {
                    gCycleBuffer.skip(1);
                    let status = gCycleBuffer.read();
                    gRspEmitter.emit('cmd_write_breakpoints_rsp', status);

                    next = 1;
                }
                break;
            default:
                console.log("Unsupported cmd: " + cmd);
                gCycleBuffer.clear();
                break;
        }
    } while (next);
}


openSerialPort();
